﻿using System;
using System.Threading;
using System.Runtime.InteropServices.Automation;

namespace ComToolkit.Data
{
    public class AdoCommand : IDisposable
    {
        public event EventHandler<ExecuteNonQueryCompletedEventArgs> ExecuteNonQueryCompleted;
        public event EventHandler<ExecuteScalarCompletedEventArgs> ExecuteScalarCompleted;
        public event EventHandler<ExecuteReaderCompletedEventArgs> ExecuteReaderCompleted;

        internal dynamic adoCommand;
        internal AdoCommand(AdoConnection connection)
        {
            adoCommand = AutomationFactory.CreateObject("ADODB.command");
            adoCommand.ActiveConnection = connection.adoConnection;
            Parameters = new AdoParameterCollection(this);
            canceled = false;
        }

        public AdoParameterCollection Parameters { get; private set; }

        /// <summary>
        /// If multiple commands should be executed, each statement must be terminated with a ; or GO.
        /// </summary>
        /// <example>Multiple commands: UPDATE mytable SET mycolumn='new value' WHERE id = 1; SELECT * FROM mytable;</example>
        public string CommandText
        {
            get { return adoCommand.CommandText; }
            set { adoCommand.CommandText = value; }
        }

        public AdoParameter CreateParameter(string name = "", object value = null)
        {
            return new AdoParameter(adoCommand.CreateParameter(), name, value);
        }

        private bool canceled;
        public void Cancel()
        {
            adoCommand.Cancel();
            canceled = true;
        }

        /// <summary>
        /// Highly experimental, be sure to create new commands for each request.
        /// </summary>
        /// <param name="userState"></param>
        public void ExecuteScalarAsync(object userState = null)
        {
            //TODO: Might need some locking mecanism.
            new Thread(() =>
                {
                    object result = 0;
                    Exception exception = null;
                    try{
                        result = ExecuteScalar();
                    }catch(Exception ex){
                        exception = ex;
                    }
                    OnExecuteScalarCompleted(new ExecuteScalarCompletedEventArgs(result, exception, canceled, userState));
                }).Start();
        }
        
        /// <summary>
        /// Highly experimental, be sure to create new commands for each request.
        /// </summary>
        /// <param name="userState"></param>
        public void ExecuteNonQueryAsync(object userState = null)
        {
            //TODO: Might need some locking mecanism.
            new Thread(() =>
                {
                    int recordsAffected = 0;
                    Exception exception = null;
                    try{
                        recordsAffected = ExecuteNonQuery();
                    }catch(Exception ex){
                        exception = ex;
                    }
                    OnExecuteNonQueryCompleted(new ExecuteNonQueryCompletedEventArgs(recordsAffected, exception, canceled, userState));
                }).Start();
        }

        /// <summary>
        /// Highly experimental, be sure to create new commands for each request.
        /// </summary>
        /// <param name="userState"></param>
        public void ExecuteReaderAsync(object userState = null)
        {
            //TODO: Might need some locking mecanism.
            new Thread(() =>
            {
                AdoDataReader reader = null;
                Exception exception = null;

                try
                {
                    reader = ExecuteReader();
                }
                catch (Exception ex)
                {
                    exception = ex;
                    
                }
                OnExecuteReaderCompleted(new ExecuteReaderCompletedEventArgs(reader, exception, canceled, userState));
            }).Start();
        }

        /// <summary>
        /// Sends the CommandText to the Connection and builds a AdoDataReader.
        /// </summary>
        /// <returns>An AdoDataReader, if multiple SQL querys is executed, the last command must contain the result.</returns>
        public AdoDataReader ExecuteReader()
        {
            PrepareCommand();
            string[] querys = CommandHelper.SplitCommands(CommandText);
            for (int i = 0; i < querys.Length; i++)
            {
                CommandText = querys[i];

                if (i == querys.Length - 1)
                {
                    dynamic rs = adoCommand.Execute(System.Type.Missing, System.Type.Missing, 1);
                    CheckErrors();
                    return new AdoDataReader(rs);
                }
                else
                {
                    adoCommand.Execute(System.Type.Missing, System.Type.Missing, 1);
                    CheckErrors();
                }
            }
            return null; //throw...
        }

        /// <summary>
        /// Executes command.
        /// </summary>
        /// <returns>Number of records affected.</returns>
        public int ExecuteNonQuery()
        {
            PrepareCommand();
            int affected = 0;
            adoCommand.Execute(ref affected, Type.Missing, 1);
            CheckErrors();
            return affected;
        }

        /// <summary>
        /// Executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored.
        /// </summary>
        /// <returns>The first column of the first row in the result set, or a null reference.</returns>
        public object ExecuteScalar()
        {
            var reader = ExecuteReader();
            return reader[0]; //TODO: checks?
        }

        private void PrepareCommand()
        {
            canceled = false;
            Parameters.ForEach(p => adoCommand.Parameters.Append(p.adoParameter));
        }

        private void CheckErrors()
        {
            if (adoCommand.ActiveConnection.Errors.Count > 0)
            {
                throw new AdoException(adoCommand.ActiveConnection.Errors, null);
            }
        }

        private void OnExecuteNonQueryCompleted(ExecuteNonQueryCompletedEventArgs args)
        {
            if (ExecuteNonQueryCompleted != null)
                ExecuteNonQueryCompleted(this, args);
        }

        private void OnExecuteScalarCompleted(ExecuteScalarCompletedEventArgs args)
        {
            if (ExecuteScalarCompleted != null)
                ExecuteScalarCompleted(this, args);
        }

        private void OnExecuteReaderCompleted(ExecuteReaderCompletedEventArgs args)
        {
            if (ExecuteReaderCompleted != null)
                ExecuteReaderCompleted(this, args);
        }

        public void Dispose()
        {
            adoCommand = null;
        }
    }
}
